// Copyright Envoy Gateway Authors
// SPDX-License-Identifier: Apache-2.0
// The full text of the Apache license is available in the LICENSE file at
// the root of the repo.

package runner

import (
	"context"
	"crypto/tls"
	"fmt"
	"os"
	"path/filepath"
	"testing"
	"time"

	cachetypes "github.com/envoyproxy/go-control-plane/pkg/cache/types"
	cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3"
	resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
	rlsconfv3 "github.com/envoyproxy/go-control-plane/ratelimit/config/ratelimit/v3"
	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
	"github.com/envoyproxy/gateway/internal/crypto"
	"github.com/envoyproxy/gateway/internal/envoygateway/config"
	"github.com/envoyproxy/gateway/internal/infrastructure/kubernetes/ratelimit"
	"github.com/envoyproxy/gateway/internal/ir"
	"github.com/envoyproxy/gateway/internal/message"
)

func Test_subscribeAndTranslate(t *testing.T) {
	t.Parallel()

	testxds := func(gwName string) *ir.Xds {
		return &ir.Xds{
			HTTP: []*ir.HTTPListener{
				{
					CoreListenerDetails: ir.CoreListenerDetails{
						Name: fmt.Sprintf("default/%s/listener-0", gwName),
					},
					Routes: []*ir.HTTPRoute{
						{
							Name: "route-0",
							Traffic: &ir.TrafficFeatures{
								RateLimit: &ir.RateLimit{
									Global: &ir.GlobalRateLimit{
										Rules: []*ir.RateLimitRule{
											{
												HeaderMatches: []*ir.StringMatch{
													{
														Name:     "x-user-id",
														Distinct: true,
													},
												},
												Limit: ir.RateLimitValue{
													Requests: 100,
													Unit:     ir.RateLimitUnit(egv1a1.RateLimitUnitMinute),
												},
											},
											{
												HeaderMatches: []*ir.StringMatch{
													{
														Name:     "x-another-user-id",
														Distinct: true,
													},
												},
												Limit: ir.RateLimitValue{
													Requests: 10,
													Unit:     ir.RateLimitUnit(egv1a1.RateLimitUnitSecond),
												},
											},
										},
									},
								},
							},
						},
						{
							Name: "route-1",
							Traffic: &ir.TrafficFeatures{
								RateLimit: &ir.RateLimit{
									Global: &ir.GlobalRateLimit{
										Rules: []*ir.RateLimitRule{
											{
												HeaderMatches: []*ir.StringMatch{
													{
														Name:     "x-user-id",
														Distinct: true,
													},
												},
												Limit: ir.RateLimitValue{
													Requests: 100,
													Unit:     ir.RateLimitUnit(egv1a1.RateLimitUnitMinute),
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		}
	}

	testRateLimitConfig := func(gwName string) *rlsconfv3.RateLimitConfig {
		return &rlsconfv3.RateLimitConfig{
			Name:   fmt.Sprintf("default/%s/listener-0", gwName),
			Domain: fmt.Sprintf("default/%s/listener-0", gwName),
			Descriptors: []*rlsconfv3.RateLimitDescriptor{
				{
					Key:   "route-0",
					Value: "route-0",
					Descriptors: []*rlsconfv3.RateLimitDescriptor{
						{
							Key: "rule-0-match-0",
							RateLimit: &rlsconfv3.RateLimitPolicy{
								Unit:            rlsconfv3.RateLimitUnit_MINUTE,
								RequestsPerUnit: 100,
							},
						},
						{
							Key: "rule-1-match-0",
							RateLimit: &rlsconfv3.RateLimitPolicy{
								Unit:            rlsconfv3.RateLimitUnit_SECOND,
								RequestsPerUnit: 10,
							},
						},
					},
				},
				{
					Key:   "route-1",
					Value: "route-1",
					Descriptors: []*rlsconfv3.RateLimitDescriptor{
						{
							Key: "rule-0-match-0",
							RateLimit: &rlsconfv3.RateLimitPolicy{
								Unit:            rlsconfv3.RateLimitUnit_MINUTE,
								RequestsPerUnit: 100,
							},
						},
					},
				},
			},
		}
	}

	testCases := []struct {
		name string
		// xdsIRs contains a list of xds updates that the runner will receive.
		xdsIRs               []message.Update[string, *ir.Xds]
		wantRateLimitConfigs map[string]cachetypes.Resource
	}{
		{
			name: "one xds is added",
			xdsIRs: []message.Update[string, *ir.Xds]{
				{
					Key:   "gw0",
					Value: testxds("gw0"),
				},
			},
			wantRateLimitConfigs: map[string]cachetypes.Resource{
				"default/gw0/listener-0": testRateLimitConfig("gw0"),
			},
		},
		{
			name: "two xds are added",
			xdsIRs: []message.Update[string, *ir.Xds]{
				{
					Key:   "gw0",
					Value: testxds("gw0"),
				},
				{
					Key:   "gw1",
					Value: testxds("gw1"),
				},
			},
			wantRateLimitConfigs: map[string]cachetypes.Resource{
				"default/gw0/listener-0": testRateLimitConfig("gw0"),
				"default/gw1/listener-0": testRateLimitConfig("gw1"),
			},
		},
		{
			name: "one xds is deleted",
			xdsIRs: []message.Update[string, *ir.Xds]{
				{
					Key:   "gw0",
					Value: testxds("gw0"),
				},
				{
					Key:   "gw1",
					Value: testxds("gw1"),
				},
				{
					Key:    "gw0",
					Delete: true,
				},
			},
			wantRateLimitConfigs: map[string]cachetypes.Resource{
				"default/gw1/listener-0": testRateLimitConfig("gw1"),
			},
		},
	}

	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			ctx, cancel := context.WithCancel(context.Background())
			defer cancel()
			xdsIR := new(message.XdsIR)
			defer xdsIR.Close()
			cfg, err := config.New(os.Stdout, os.Stderr)
			require.NoError(t, err)

			r := New(&Config{
				Server:       *cfg,
				XdsIR:        xdsIR,
				RunnerErrors: new(message.RunnerErrors),
				cache:        cachev3.NewSnapshotCache(false, cachev3.IDHash{}, nil),
			})

			c := xdsIR.Subscribe(ctx)
			go r.translateFromSubscription(ctx, c)

			for _, xds := range tt.xdsIRs {
				if xds.Delete {
					xdsIR.Delete(xds.Key)
					continue
				}
				xdsIR.Store(xds.Key, xds.Value)
			}

			diff := ""
			if !assert.Eventually(t, func() bool {
				rs, err := r.cache.GetSnapshot(ratelimit.InfraName)
				if err != nil {
					t.Logf("failed to get snapshot: %v", err)
					return false
				}

				diff = cmp.Diff(tt.wantRateLimitConfigs, rs.GetResources(resourcev3.RateLimitConfigType), cmpopts.IgnoreUnexported(rlsconfv3.RateLimitConfig{}, rlsconfv3.RateLimitDescriptor{}, rlsconfv3.RateLimitPolicy{}))
				return diff == ""
			}, time.Second*10, time.Second) {
				t.Fatalf("snapshot mismatch (-want +got):\n%s", diff)
			}
		})
	}
}

func TestLoadTLSConfig_HostMode(t *testing.T) {
	// Create temporary directory structure for certs using t.TempDir()
	configHome := t.TempDir()
	certsDir := filepath.Join(configHome, "certs", "envoy-gateway")
	require.NoError(t, os.MkdirAll(certsDir, 0o750))

	// Create test certificates using internal/crypto package
	cfg, err := config.New(os.Stdout, os.Stderr)
	require.NoError(t, err)

	// Generate certificates with default provider (crypto.GenerateCerts only supports Kubernetes)
	certs, err := crypto.GenerateCerts(cfg)
	require.NoError(t, err)

	// Write certificates to temp directory
	caFile := filepath.Join(certsDir, "ca.crt")
	certFile := filepath.Join(certsDir, "tls.crt")
	keyFile := filepath.Join(certsDir, "tls.key")

	require.NoError(t, os.WriteFile(caFile, certs.CACertificate, 0o600))
	require.NoError(t, os.WriteFile(certFile, certs.EnvoyGatewayCertificate, 0o600))
	require.NoError(t, os.WriteFile(keyFile, certs.EnvoyGatewayPrivateKey, 0o600))

	// Configure host mode with custom configHome (certs are stored in configHome)
	// MUST be set BEFORE creating Runner since Config{Server: *cfg} makes a copy
	cfg.EnvoyGateway.Provider = &egv1a1.EnvoyGatewayProvider{
		Type: egv1a1.ProviderTypeCustom,
		Custom: &egv1a1.EnvoyGatewayCustomProvider{
			Infrastructure: &egv1a1.EnvoyGatewayInfrastructureProvider{
				Type: egv1a1.InfrastructureProviderTypeHost,
				Host: &egv1a1.EnvoyGatewayHostInfrastructureProvider{
					ConfigHome: &configHome,
				},
			},
		},
	}

	r := &Runner{
		Config: Config{
			Server: *cfg,
		},
	}

	// Test loadTLSConfig with host mode
	tlsConfig, err := r.loadTLSConfig()
	require.NoError(t, err)
	require.NotNil(t, tlsConfig)

	// Verify TLS config properties
	// crypto.LoadTLSConfig uses GetConfigForClient callback to load certs on demand
	require.NotNil(t, tlsConfig.GetConfigForClient)
	require.Equal(t, tls.RequireAndVerifyClientCert, tlsConfig.ClientAuth)
	require.Equal(t, uint16(tls.VersionTLS13), tlsConfig.MinVersion)
}
